package require TclOO

##Create a channeled interface to a program.
#
oo::class create Interface {
    constructor {} {
	my setDir [pwd]
	my log input stdout
	my log output stdout
	my loglevel input 0 
	my loglevel output 2
	my variable compress
	set compress 0
    }
    method setDir {} {}
    method openChannel {} {}
    method log {} {}
    method loglevel {} {}
    destructor {
	my variable channel
	my variable log_output
	flush $log_output
	if {[string compare $log_output stdout] !=0} { close $log_output}
	close $channel
    }
}

# core utilities

##Establish a I/O channel with the binary file.
#
oo::define Interface method openChannel { bin } {
    my variable channel
    my variable connected
    my variable buffer
    my variable incoming
    my variable workingDir
    set incoming ""
    my print 1 "Opening $bin at [clock format [clock seconds] -format "%D %H:%M:%S" ] "
    set olddir [pwd]
    cd $workingDir
    set channel [open "|$bin" r+]
    cd $olddir
    fconfigure $channel -blocking 0
    my print 1 "Opened"
}

##Send a command/input to the opened program.
oo::define Interface method send { msg} {
    my variable channel
    my variable log_input
    my variable printlvl
    my print 1 "Sending: $msg"
    set level 1
    if {$level < $printlvl(input,high)} {
	if { $level >= $printlvl(input,low)} {
            puts $log_input $msg
            flush $log_input
        } else {
	    puts $msg
        }
    }
    puts $channel $msg
    #puts -nonewline $channel $msg ?
    flush $channel
}

## Wait for the expect message to appear in the output of the program.
# expect { output1 {action1} output2 {action2} }
# If any expectation is found, the corresponding action will be execuated.
# The action can contain flow-control key word "exp_break" and "exp_continue".
# The exp_continue will clear make expect enters another listening cycle.
# If the expected message does not appear, the program will hang.
oo::define Interface method expect { pairs} {
    my variable doneRead
    my variable channel
    my variable incoming
    my variable buffer
    set timeout 600
    fileevent $channel readable "[self] readBuffer $channel"
    set buffer ""
    set time [clock clicks -milliseconds]
    while 1 {
	if {[ expr [clock clicks -milliseconds] -$time] > [expr $timeout*1000]} break
	vwait [my varname doneRead]
	set time [clock clicks -milliseconds]
	my print 1 "Incoming $incoming"
	append buffer $incoming
	set status [my check $buffer $pairs]
	switch $status {
	    hasMatch { return }
	    end_expect { return }
	    continue { set buffer "" }
	}
    }
}

# setup commands
oo::define Interface method setDir { dir} {
    my variable workingDir
    if ![file exists $dir] { exec mkdir $dir}
    set workingDir $dir
}

oo::define Interface method log { type fname} {
    my variable log_input
    my variable log_output
    my variable workingDir
    my variable compress
    switch $type {
	input { if {[lsearch {stdout stderr} $fname] >=0} { 
	    set log_input $fname
	} else {
	    set log_input [open $workingDir/$fname w]
	}
	}
	output { if {[lsearch {stdout stderr} $fname] >=0} { 
	    set log_output $fname
	} else {
	    if $compress {
	    set log_output [zlib push gzip [open $workingDir/$fname.gz wb]]
#            set log_output [open "|gzip - $workingDir/$fname.gz" w ]
	    } else {
	    set log_output [open $workingDir/$fname w]
	    }
	    
	}
	}
    }
}
## Make the output be compressed using zlib.
# Currently cannot work with multiple thread because zlib is not thread safe.
oo::define Interface method compressOutput {} {
   my variable compress
   set compress 1
}

## Set up the print level for certain log.
# The first paramter is the log name.
# The seond paramter is the minimum level to print a message to screen.
# THe third paramter is the minimum level to print a message to a file. 
oo::define Interface method loglevel { type noprint {screen 1}} {
    my variable printlvl
    set printlvl($type,low) $screen
    set printlvl($type,high) $noprint
}

## set up the wait time for the expect to wait for output.
oo::define Interface method timeout { time } {
    my variable timeout
    set timeout $time 
}
# helper function

## read from channel
oo::define Interface method readBuffer { channel } {
    my variable doneRead
    my variable incoming
    my variable buffer
    set incoming ""
    set blocksize 10
    while 1 {
	set block [read $channel $blocksize]
	if { [string length $block] < $blocksize } {
	    append incoming $block
	    break
	} else {
	    append incoming $block
	}
    }
    if {[string length $incoming] > 0} {set doneRead 1}
}
## check if the incomming output has the expected messages.
#If there is any expection is found, execuate the action followed by the message.
#The action can contain flow-control key word "exp_break" and "exp_continue".
#This would cause the check to return "end_expect" status and "continue" status.
#If not, return nothing.
oo::define Interface method check { incoming pairs} {
    set hasMatch 0
    foreach { expectation action } $pairs {
	set action [string map { exp_break "return end_expect"} $action]
	set action [string map [list  my [self]] $action]
	set action [string map { exp_continue "return continue "} $action]
	if [regexp -all $expectation $incoming matched] {
	    set hasMatch 1
	    upvar 2 matched Matched
	    upvar 2 buffer Buffer
	    set Buffer $incoming
	    set Matched $matched
	    uplevel 2 $action
	}
	
    }
    if { $hasMatch == 1} { return hasMatch}
}

##
oo::define Interface method print { level msg } {
    my variable printlvl
    my variable log_output
    my variable compress
#    if $compress { set msg [zlib gzip $msg -header {type text}]}
    if {$level < $printlvl(output,high)} {
	if { $level >= $printlvl(output,low)} {
	    puts $log_output $msg
	    flush $log_output
	} else {
	    puts $msg
	}
    }
}
